Перейти к основному содержимому

5.16. История языка

Разработчику Архитектору

Lisp

История языка

1. Введение: место Lisp в истории программирования

Язык Lisp (от LISt Processor) занимает уникальное положение в истории программирования: он не только один из старейших языков высокого уровня, сохранивших активное применение, но и наиболее последовательная реализация идей, заложенных в основания математической логики и теории вычислений. Его появление в 1958 году ознаменовало переход от императивных моделей вычислений, ориентированных на машинную архитектуру фон Неймана, к декларативной парадигме, в которой программа рассматривается не как последовательность команд процессору, а как выражение преобразований над структурами данных. Lisp стал первым языком, в котором синтаксис был полностью выведен из семантики: код и данные выражаются в единой, рекурсивно определяемой структуре — связном списке, — что открывает путь к глубокой интроспекции и метапрограммированию.

Важно подчеркнуть: Lisp не был результатом инженерной оптимизации или промышленного заказа. Он возник как инструмент формализации интеллектуальных процессов — в рамках исследований в области искусственного интеллекта, предпринятых Джоном Маккарти и его коллегами в Массачусетском технологическом институте (MIT). Таким образом, Lisp изначально концептуализировался не как средство автоматизации рутинных задач, а как язык мыслей, пригодный для моделирования рассуждений — и эта философская установка определила его архитектурные особенности на десятилетия вперёд.


2. Предыстория: логические основания и вычислимость

Чтобы понять генезис Lisp, необходимо обратиться к событиям, предшествовавшим его созданию на два десятилетия. В 1930-х годах Алонзо Чёрч предложил лямбда-исчисление — формальную систему, предназначенную для описания функций и их аппликации. В отличие от машин Тьюринга, фокусировавшихся на механических шагах вычисления, лямбда-исчисление оперировало абстрактными преобразованиями: переменная связывалась, выражение подставлялось, редукция выполнялась до нормальной формы. Эта система оказалась эквивалентной по вычислительной мощности машине Тьюринга (теорема Чёрча–Тьюринга), но её выразительность была ближе к математической практике, чем к архитектуре ЭВМ.

Одновременно с этим в работах Курта Гёделя и Стефана Клини развивалась теория рекурсивных функций, где центральную роль играли примитивная и частичная рекурсия. Именно рекурсия, а не итерация, стала естественным способом определения вычислений в теоретических моделях.

Маккарти, будучи знакомым с этими работами, стремился создать практический язык, в котором:

  • функции были бы первоклассными объектами (могли передаваться как аргументы, возвращаться как результаты);
  • программы строились бы рекурсивно, без опоры на изменяемое состояние;
  • данные и код имели бы единое представление, допускающее динамическое построение и анализ выражений.

Эти требования вытекали не из соображений удобства, а из попытки реализовать вычислительную модель, адекватную человеческому рассуждению — в рамках проекта по автоматическому доказательству теорем и игре в шахматы.


3. Создание: 1958–1960 годы

Первая версия Lisp была разработана Джоном Маккарти совместно с Марвином Мински, Аланом Ньюэллом и другими участниками Dartmouth Summer Research Project on Artificial Intelligence (1956), где и был впервые сформулирован термин «искусственный интеллект». Однако реализация задержалась: на тот момент не существовало компиляторов, способных обрабатывать рекурсивные вызовы, а стек вызовов ещё не был общепринятой техникой (его введение в практику приписывают работе Дейкстры 1960 года).

Маккарти обходит это ограничение, предложив интерпретатор, написанный вручную на ассемблере для IBM 704. Этот интерпретатор (иногда именуемый Lisp I) поддерживал лишь базовые конструкции:

  • символы (атомы) — неизменяемые именованные сущности;
  • списки — упорядоченные пары (car . cdr), где car — первый элемент, cdr — «остаток» списка;
  • семь примитивных функций: atom, eq, car, cdr, cons, cond, lambda.

Замечательно, что уже в этой первой версии присутствовала поддержка анонимных функций через lambda — заимствование напрямую из лямбда-исчисления Чёрча. Вызов функции записывался в префиксной нотации: (f x y), что гарантировало однозначный синтаксический разбор без приоритетов операций.

Спецификация cond (условное выражение) позволяла определять рекурсивные функции, такие как факториал или вычисление чисел Фибоначчи, без использования циклов — в полном соответствии с теоретико-рекурсивной парадигмой. Маккарти продемонстрировал, что с помощью cond и lambda можно выразить любое вычисление, реализуемое машиной Тьюринга.

В 1960 году Маккарти публикует статью «Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I» — первый формальный отчёт о Lisp. Несмотря на отсутствие второй части (оставшейся ненаписанной), эта работа считается основополагающей. В ней впервые вводится понятие вычисления как символьного преобразования, где программа — не последовательность инструкций, а выражение, подлежащее редукции.


4. Ранняя эволюция: Lisp 1.5 и становление экосистемы

К 1962 году появляется Lisp 1.5 — первая широко распространённая версия, разработанная Стивеном Расселом, Даниэлем Эдвардсом и другими под руководством Маккарти. В отличие от интерпретатора 1958 года, Lisp 1.5 включал:

  • компилятор (хотя и примитивный), переводящий lambda-выражения в машинный код;
  • механизм динамической области видимости (в отличие от лексической, которая появится позже);
  • встроенную поддержку символьных вычислений, включая quote и eval.

Именно в Lisp 1.5 впервые реализуется функция eval — интерпретатор Lisp, написанный на самом Lisp. Это событие имеет фундаментальное значение: оно демонстрирует рефлексивность языка — способность к самоописанию. Функция (eval expr) принимает на вход список, представляющий выражение, и выполняет его как программу. Тем самым граница между данными и кодом становится условной: любой список потенциально исполняем; любая программа — это просто структура данных, ожидающая eval.

С появлением eval и quote возникает макросистема в зачаточной форме: программист может строить выражения во время выполнения, манипулируя ими как списками, а затем передавать результат eval. Хотя полноценные синтаксические макросы (как в Common Lisp) появятся лишь в 1970-х, уже в начале 1960-х исследователи MIT и Стэнфорда активно использовали этот механизм для создания domain-specific languages (DSL) внутри Lisp — например, для описания правил в экспертных системах.

К середине 1960-х Lisp становится основным языком исследований в области ИИ. На нём реализуются:

  • SAINT (Symbolic Automatic INTegrator) — система символьного интегрирования (1961);
  • STUDENT — программа решения текстовых задач по алгебре (1964);
  • SHRDLU — система понимания естественного языка в ограниченной «мире блоков» (1968–1970).

Эти проекты подтверждают гипотезу Маккарти: символьные манипуляции через рекурсивные функции действительно пригодны для моделирования когнитивных процессов.


5. Дивергенция: MIT, Stanford, и рождение диалектов

К концу 1960-х происходит фрагментация Lisp-сообщества по институциональным и философским линиям.

В MIT развивается Maclisp — мощный, но нестандартизованный диалект, ориентированный на производительность и интеграцию с операционной системой ITS (Incompatible Timesharing System). Maclisp вводит ряд нововведений: хешированные символы, компиляцию в объектный код, поддержку чисел с плавающей точкой и комплексных чисел. Он становится основой для таких систем, как MACSYMA — первой крупной системой компьютерной алгебры.

Одновременно в Стэнфорде Джон Маккарти работает над Stanford Lisp, стремясь к большей строгости и логической чистоте. Позже его ученики — Гай Стил и Джералд Сассман — разрабатывают Scheme (1975), который станет радикальной переработкой Lisp в духе лямбда-исчисления.

Scheme вводит:

  • лексическую область видимости (в отличие от динамической в Maclisp и Lisp 1.5);
  • единое пространство имён функций и переменных («Lisp-1», в отличие от «Lisp-2», где функции и значения раздельны — как в Common Lisp);
  • хвостовую рекурсию как итерацию: любой хвостовой вызов оптимизируется в безстековый переход, что делает рекурсию эффективной заменой циклам;
  • минимализм специальных форм: только lambda, if, define, quote, set!, begin.

Scheme становится языком обучения (например, в учебнике Structure and Interpretation of Computer Programs, 1985), подчёркивая, что язык программирования — это не инструмент автоматизации, а средство формулирования идей.


6. Стандартизация и Common Lisp

К середине 1970-х в экосистеме Lisp существовало более двух десятков несовместимых реализаций: Maclisp, Interlisp (развиваемый в Xerox PARC и Беркли), Lisp Machine Lisp (на базе специализированных ЭВМ от Symbolics и LMI), NIL (New Implementation of Lisp), а также Scheme и его варианты. Отсутствие переносимости тормозило развитие библиотек и промышленное внедрение. В 1981 году по инициативе DARPA (Управления перспективных исследовательских проектов Министерства обороны США) была запущена работа по созданию единого стандарта. Координацией занималась Special Interest Group on Lisp (SIGLISP) при ACM; главным редактором спецификации стал Гай Стил — автор The Hacker’s Dictionary и соавтор Scheme.

Результатом стал Common Lisp — не просто компромисс между диалектами, а попытка систематизировать 25 лет эмпирического опыта. Спецификация Common Lisp, опубликованная в 1984 году как ANSI X3.226-1994 (окончательно утверждена в 1994), представляла собой:

  • единый, богатый набор встроенных типов: символы, числа (целые, рациональные, с плавающей точкой, комплексные), последовательности (списки и векторы), хэш-таблицы, структуры, потоки;
  • объектную систему CLOS (Common Lisp Object System), разработанную в конце 1980-х и включённую в стандарт. CLOS отличался от классических ООП-моделей: он поддерживал множественное наследование, обобщённые функции (generic functions), методы, диспетчеризуемые по любому аргументу (multimethods), и метаклассы. Это делало CLOS ближе к подходам в prototype-based языках и современным системам вроде Julia, чем к Java или C++;
  • мощную систему макросов, основанную на шаблонах (macroexpand-time pattern matching), а не на простой текстовой подстановке. Макросы Common Lisp работают на уровне s-выражений и запускаются до компиляции, что позволяет создавать синтаксические конструкции, неотличимые от встроенных;
  • условную систему, основанную на условных выражениях (cond, case) и обобщённой системе обработки ошибок (conditions and restarts), позволяющей не просто перехватывать исключения, но и предлагать возобновления (restarts) — стратегии восстановления, выбираемые динамически в точке возникновения ошибки.

Важно отметить, что Common Lisp не отвергал динамическую природу Lisp. Напротив, он закреплял её: компилятор мог вызываться во время выполнения (compile, compile-file), функции перекомпилировались «на лету», а отладчик (SLDB — Superior Lisp Debugger) предоставлял интерактивный доступ к стеку вызовов с возможностью модификации кода и данных в реальном времени. Эта интерактивность — не побочный эффект, а архитектурный принцип: среда программирования рассматривалась как единый, изменяемый организм, а не как набор статичных файлов.

Реализации Common Lisp (например, SBCL, Clozure CL, Allegro CL, CLISP) до сих пор используются в нишевых, но критически важных областях: авионика (Boeing, Lockheed Martin), финансовые системы (ITA Software, позднее приобретённая Google), робототехника (NASA), а также в системах, требующих длительного апгрейда без остановки (hot code swapping).


7. Lisp и эпоха специализированных машин

В 1970–1980-х годах развитие Lisp напрямую стимулировало создание Lisp-машин — аппаратных платформ, оптимизированных под выполнение Lisp-кода. Компании Symbolics и Lisp Machines Inc. (LMI) выпускали рабочие станции, в которых:

  • указатели были тегированными: 2–3 бита в каждом машинном слове указывали тип данных (символ, список, число, функция и т.д.), что позволяло реализовать динамическую типизацию без накладных расходов;
  • сборка мусора была аппаратно ускорена;
  • микрокод процессора включал инструкции для car, cdr, cons, apply;
  • вся операционная система (Genera у Symbolics) была написана на Lisp — включая графический интерфейс, сетевой стек, редактор и отладчик.

Это был апогей идеи Маккарти: полная интеграция языка и среды. Однако к концу 1980-х рост производительности универсальных процессоров (в частности, RISC-архитектур) сделал Lisp-машины экономически невыгодными. Рынок сместился в сторону Unix-совместимых систем и языков C/C++. Lisp утратил статус «языка будущего» в массовом сознании, но сохранил влияние на уровне идей.


8. Современные воплощения: Clojure и его философия

В 2007 году Рич Хикки представляет Clojure — диалект Lisp, изначально созданный для Java Virtual Machine (JVM). Clojure нельзя рассматривать как «ещё один Lisp»: это реинтерпретация принципов Lisp в контексте многопоточности, неизменяемости и практического промышленного применения.

Ключевые идеи Clojure:

  • неизменяемые структуры данных по умолчанию: векторы, списки, множества, отображения реализованы как persistent data structures с эффективным разделением памяти (structural sharing). Модификация создаёт новую версию, не затрагивая исходную — это устраняет гонки данных без блокировок;
  • модель параллелизма через ссылки: ref, atom, agent, var — абстракции, явно разделяющие идентичность, состояние и значение. Например, atom обеспечивает атомарное обновление состояния с помощью функции-трансформера; ref работает в рамках программных транзакций (STM — Software Transactional Memory);
  • хост-интеграция: Clojure не стремится заменить экосистему хоста. На JVM он использует весь стек Java: библиотеки, инструменты сборки (Maven, Leiningen), профайлеры. Аналогично, ClojureScript компилируется в JavaScript и интегрируется с React, Node.js и т.д.;
  • макросы с гигиеническим контролем: хотя синтаксис макросов близок к Common Lisp, Clojure добавляет механизмы предотвращения захвата имён (gensym по умолчанию, syntax-quote, auto-gensym), повышая безопасность метапрограммирования;
  • отказ от лексем с побочными эффектами в «чистом» коде: вызовы вроде (println …) явно выделяются (хотя и не запрещены), поощряя функциональный стиль.

Clojure — не попытка «оживить Lisp», а синтез: он берёт гомогенность синтаксиса и выразительность макросов из Lisp, строгую типизацию и компиляцию — из ML-семейства (через типовые подсказки), а модель параллелизма — из исследований по конкурирующим системам (например, работы Лесли Лэмпорта). Его успех в компаниях уровня Walmart, Nubank, CircleCI свидетельствует: принципы Lisp остаются жизнеспособными при условии адаптации к современным ограничениям — прежде всего, к задачам масштабируемости и сопровождаемости.


9. Влияние Lisp на другие языки: Haskell, ML, и за их пределами

Lisp оказал косвенное, но глубокое влияние на развитие функциональных языков, хотя и не является их прямым предком (этой роли придерживается лямбда-исчисление).

  • Haskell унаследовал от Lisp идею выражений как основной единицы программы, а также использование рекурсии вместо итерации. Однако, в отличие от Lisp, Haskell строго разделяет чистые вычисления и побочные эффекты (через монады), что делает его ближе к теоретической чистоте Чёрча, чем к прагматизму Маккарти. Тем не менее, система Template Haskell — это метапрограммирование, вдохновлённое лисповскими макросами: код генерируется во время компиляции через исполнение Haskell-кода.

  • ML-семейство (Standard ML, OCaml, F#) заимствовало у Lisp:

    • интерактивную среду (REPL), ставшую обязательной для функциональных языков;
    • автоматическое управление памятью (сборка мусора), впервые реализованную в Lisp 1.5;
    • полиморфные типы и вывод типов — хотя формально эта идея возникла в системе Hindley–Milner, её практическая реализация была проверена на Lisp-подобных системах (например, в Nuprl и LISP/ML).
  • JavaScript и Python: несмотря на императивную основу, оба языка включают элементы, прямо восходящие к Lisp:

    • функции как объекты первого класса;
    • замыкания (lexical closures), реализованные в Scheme и перенесённые в JavaScript через влияние Scheme-разработчиков (например, Брендана Эйха);
    • eval — хотя и считается антипаттерном, его существование — прямая дань лисповской рефлексивности.
  • Rust, несмотря на системную направленность, использует макросы на основе синтаксических деревьев (macro_rules!, declarative macros), а не текстовой подстановки — подход, прямо заимствованный из Scheme и Common Lisp. Даже термин hygiene в документации Rust ссылается на гигиенические макросы Scheme.

Таким образом, Lisp — не столько «язык, который все забыли», сколько невидимая основа: его идеи, очищенные от специфики синтаксиса, пронизывают современные языки через концепции, ставшие общепринятыми.


10. Lisp как методология мышления

Язык Lisp выжил не благодаря синтаксису (скобки остаются барьером для многих), а благодаря гомоморфизму между структурой данных и структурой программы. Эта особенность позволяет рассматривать программу не как статический артефакт, а как динамическую модель, которую можно анализировать, преобразовывать и расширять в процессе выполнения. В эпоху, когда доминируют компонентные архитектуры, конфигурации через код (Infrastructure as Code), генеративное программирование и LLM-augmented development, эта особенность приобретает новую актуальность.

Lisp не предполагает, что программист знает всё заранее. Он предлагает среду для постепенного уточнения: сначала — прототип на интерпретаторе, затем — макросы, формирующие DSL, затем — компиляция в эффективный код, затем — интеграция с хост-системой. Такой цикл ближе к научному методу, чем к инженерному производству.

Маккарти писал: «Lisp was not the product of a committee, nor was it designed to meet a list of requirements. It came out of a theory of computation». Сегодня, когда теория вычислений сталкивается с реальностью распределённых систем, нейросетевых моделей и квантовых алгоритмов, эта теоретическая основа — не роскошь, а необходимость. И в этом смысле Lisp остаётся не реликтом, а ориентиром.